﻿using System.Collections.Generic;
using TheSnakeGame.Engine.GameObjects;
using System.Linq;
using System;

namespace TheSnakeGame.Engine
{
    /// <summary>
    /// The Engine. Manages the game flow
    /// </summary>
    public class GameEngine
    {
        private int width;
        private int height;
        private IList<GameObject>[,] gameMap;

        public GameEngine(int width, int height)
            : this(width, height, new FoodManager(), new RandomGenerator())
        {
        }

        public GameEngine(int width, int height, IFoodManager foodManager, IRandomGenerator randomGenerator)
        {
            this.width = width;
            this.height = height;
            this.gameMap = new List<GameObject>[width, height];

            this.GameMap.Initialize((x, y) => new List<GameObject>());
            this.GameObjects = new List<GameObject>();
            this.BarierWalls = new List<BarierObject>();
            this.FoodManager = foodManager;
            this.RandomGenerator = randomGenerator;
        }

        /// <summary>
        /// Represent the snake head 
        /// </summary>
        public SnakeHead Snake
        {
            get;
            set;
        }

        /// <summary>
        /// A List of all game objects
        /// </summary>
        public IList<GameObject> GameObjects
        {
            get;
            set;
        }

        /// <summary>
        /// A subset of GameObjects that contains all walls
        /// </summary>
        public IList<BarierObject> BarierWalls
        {
            get;
            set;
        }

        /// <summary>
        /// Used for easier presentation of the current state of each object. Also usefull when determning which object are interacting with each other
        /// </summary>
        public IList<GameObject>[,] GameMap
        {
            get { return this.gameMap; }
        }

        public IFoodManager FoodManager 
        { 
            get; 
            set; 
        }

        public IRandomGenerator RandomGenerator 
        { 
            get; 
            set; 
        }

        /// <summary>
        /// Performs standart initialization adding initial objects e.g. walls, first food, snake and so on
        /// </summary>
        public virtual void PerfostmStandartInitialization()
        {
            this.AddBarierWalls();
            this.AddFood(1);
            this.AddInitialSnake();
           

            this.RefreshGameMap();
        }

        public virtual void AddFood(int cellsCount)
        {
            IList<Point> coordinatesOfFood = this.GetRandomFreeCells(cellsCount);
            IList<Food> foods = this.FoodManager.GetFoods(coordinatesOfFood);

            foreach (var food in foods)
            {
                this.GameObjects.Add(food as GameObject);
            }
        }

        /// <summary>
        ///  Each time this method is called, the game engine will process each obejct in the GameObject collection
        /// </summary>
        public virtual void ProcessGameTick()
        {
            // First phase - do object interactions

            // Second Phase - execute each game object action
            foreach (var gameObj in this.GameObjects)
            {
                gameObj.DoAction();
            }

            this.PreccessMap();
            this.ProccessFood();
            this.RefreshGameMap();
        }

        /// <summary>
        /// Remove all objects that are marked for distuction
        /// </summary>
        private void PreccessMap()
        {
            for (int i = 0; i < this.GameObjects.Count; i++)
            {
                if (this.GameObjects[i].IsForDestruction == true)
                {
                    this.GameObjects.Remove(this.GameObjects[i]);
                }
            }
        }

        /// <summary>
        /// Remove marked food and instantiate new one
        /// </summary>
        public void ProccessFood()
        {
            bool isFoodRequared = this.FoodManager.IsNewFoodRequared();

            if (isFoodRequared)
            {
                int foodCount = this.FoodManager.GetRequerdFoodCount();
                this.AddFood(foodCount);
                this.FoodManager.DeleteMarkedForDestruction();
            }
        }

        public IList<Point> GetRandomFreeCells(int wantedCellsCount)
        {
            List<Point> allFreeCells = GetFreePositions();

            IList<Point> takenFreeCells = new List<Point>();
            
            for (int i = 0; i < wantedCellsCount; i++)
            {
                int position = this.RandomGenerator.GetRandomNumberInRange(0, allFreeCells.Count);

                if (takenFreeCells.Contains(allFreeCells[position]) ||
                    (this.GameObjects.Any(x=>x.Position.X == allFreeCells[position].X && 
                        x.Position.Y == allFreeCells[position].Y)))
                {
                    i--;
                    continue;
                }

                takenFreeCells.Add(allFreeCells[position]);
            }

            return takenFreeCells;
        }

        public List<Point> GetFreePositions()
        {
            List<Point> allFreeCells = new List<Point>();

            for (int i = 0; i < this.GameMap.GetLength(0); i++)
            {
                for (int j = 0; j < this.GameMap.GetLength(1); j++)
                {
                    IList<GameObject> cellObjects = this.GameMap[i, j];

                    if (cellObjects.Count == 0)
                    {
                        Point point = new Point(i, j);
                        allFreeCells.Add(point);
                    }
                }
            }

            return allFreeCells;
        }

        /// <summary>
        /// Adds barier walls at the borders of the map.
        /// </summary>
        /// <remarks>
        /// The caller of this method is also responsible to call RefreshGameMap() when map is ready for synchronization
        /// </remarks>
        public virtual void AddBarierWalls()
        {
            var wallFactory = new WallFactory(this.width, this.height);
            var wallObjects = wallFactory.GetWalls();

            var comparer = new PointComperer();
            if (this.GameObjects.Any(x => wallObjects.Select(wo => wo.Position).Contains(x.Position, comparer)))
            {
                throw new InvalidOperationException("You cannot add walls over each other");
            }

            foreach (var wallPiece in wallObjects)
            {
                this.GameObjects.Add(wallPiece);
                this.BarierWalls.Add(wallPiece);
            }
        }

        /// <summary>
        /// Add the snake at the center of the game field
        /// </summary>
        public virtual void AddInitialSnake()
        {
            var x = this.width / 2;
            var y = this.height / 2;
            var length = Math.Max(3, (this.width + this.height) / 20);
            var snake = new SnakeHead(new Point(x, y), length);

            var comparer = new PointComperer();
            if (this.GameObjects.Any(go => snake.Body.Select(b => b.Position).Contains(go.Position, comparer)) ||
                this.GameObjects.Select(go=>go.Position).Contains(snake.Position, comparer))
            {
                throw new InvalidOperationException("Snake can only be added at free positions");
            }

            this.Snake = snake;
            this.GameObjects.Add(snake);
            foreach (var snakePart in snake.Body)
            {
                this.GameObjects.Add(snakePart);
            }
        }

        /// <summary>
        /// Call to force refresh the GameMap with the latest position of each object
        /// </summary>
        internal protected virtual void RefreshGameMap()
        {
            this.GameMap.Initialize((x, y) => new List<GameObject>());
            foreach (var gameObject in this.GameObjects)
            {
                var x = gameObject.Position.X;
                var y = gameObject.Position.Y;
                this.GameMap[x, y].Add(gameObject);
            }
        }
    }
}
